干货|不断打磨和提升——PC Web直播播放器技术演进
0.导读
PC Web直播播放器(下文均称作 “播放器”)主要支撑爱奇艺直播频道下游戏直播、小剧场以及大型晚会、明星发布会等,同时也应用于内部直播、监播预览。播放器实现了不同设备、不同浏览器下,对不同封装格式的直播流的播放,以及对广告、多语言、弹幕等功能的拆分和整合。架构上,播放器先后经过了完全Flash,Flash和H5混合架构,以及业务逻辑完全H5化的三个过程。
经团队不断打磨,改造后的通用化播放器架构已经形成,播放能力的性能和可用性得以大幅提高,播放器的易用性和用户体验也在不断提升。下文将就直播播放器应对业务和技术环境的变化所做的实践,特别是整合播放器的设计和实现做详细介绍。
1.Flash时代
H5标准之前,Flash提供了主流的音视频播放解决方案。无论是基于TCP传输的RTMP,或者基于HTTP的FLV,Flash都提供了方便好用的接口,开发者基本不用关心编解码的细节。
播放器的主要内容都在业务逻辑的处理上,此时的架构比较简单。
图1 Flash播放器架构图
2. 混合架构时代
HTML5标准发布以来,至2015年左右,主流浏览器(除IE外)基本都实现了对H5的支持。同时,Flash经常爆出安全和性能的问题,开发者社区活跃度下降。作为非标准时代的产物,Flash也到了落幕的时候:
1. 2016年,B站flv.js[^1]的出现提供了一种H5直播的可能
2. 2017年7月25日,Adobe宣告2020年之后不再更新和分发Flash[^2]
17年5月的时候,H5播放作为团队内部一个调研方向逐渐展开,Adobe声明发布之后,作为一个优先项目提上日程。此时整个播放器的架构设计成:
图2 混合播放器架构
PlayerSDK
作为最外层的壳,是一些轻量级逻辑代码,提供与外部通信的接口和事件,并根据浏览器兼容性路由到不同的播放器上去。H5Player
和FlashPlayer
相互独立,各自有自己的业务处理和播放引擎。
本阶段主要针对浏览器兼容性和体验上的问题进行打磨,比如在最新H5Player
下可能出现的音画不同步、弱网直播延迟、浏览器拉流兼容、H5播放的QoS埋点和监控。同时将播放重心逐渐过渡到H5上来,Flash播放器作为一个兜底方案。
3. 整合播放器时代
3.0 问题和解决思路
H5Player
上线以来,经历了大大小小的几次填坑之后,H5播放逐渐稳定。但是业务上的问题逐渐显现:
对于每一个产品需求,基本都要实现两份(Flash和H5),这直接导致了我们开发量的上升
与此同时,团队所负责的业务范围进一步扩大,新业务提出了更多的要求:
1. 需要支持更多封装格式和拉流方式
2. 需要支持移动设备的浏览观看
3. 需要处理多语言、广告等附加功能
新老业务两块主要业务线因为需求差异以及历史原因,分别是两个独立的的播放器;同时,每条业务线在低版本浏览器下,必须使用Flash播放器来支持播放和相关的上层业务。
面对这些现实问题,一个想法在团队内部产生:
我们能否把所有播放器整合起来,同时兼顾业务逻辑的扩展性?
最终整合播放器遵循了以下的设计思路和解决方案:
首先,我们提出“插件化”的概念,把各个业务的功能抽离成一个个插件,并使得各种插件在不同的业务形态上形成复用,从逻辑上消除了不同业务线或需求造成的播放器差异,只通过初始化配置的方式,选择不同的插件,就能达到实际需求
其次,我们提出“去Flash化”——实际结果就是把Flash的应用范围从独立播放器缩小为Flash播放内核,内核的上层统一使用JavaScript进行实现;这样就完全消除了每个业务需求都要在低版本上使用Flash再实现一次的成本。
另外,针对上述说的客户端场景不同,使用EngineSDK
来做作为播放内核的统一代理,来包装不同场景下的多个内核,做到统一管理,并相互隔离。
3.1 播放器逻辑分层
播放器按照角色拆分为三层:
1. UniPlayer,SDK部分,负责与外部通信的接口,以及不同业务插件功能的配置和组合
2. PlayerCore, 供插件使用的轻量级公用接口,以及插件状态管理
3. Plugin,各个独立的业务逻辑,比如播控、播放引擎、弹幕等
图3 整合播放器逻辑分层
其中插件是主要业务单元和扩展单元。对于逻辑复杂的插件,其具体实现可以封装成一个小型的SDK,通过Plugin
的load
方法动态加载。
```js /** * 插件管理基类 */ class PluginBase extends Emitter { constructor(container, player){}
//生命周期方法
load(){}
init(e){}
destroy(){}
//事件管理方法
addPlayerEvent(type, handler){}
removeAllPlayerEvents(){}
//状态管理方法
getState() {}
getType(){}
}
export default PluginBase; ```
3.2 插件通信
Plugin
之间相互独立,无法直接通信。Plugin
与PlayerCore
之间进行双向通信。插件内部的状态保存到公共State
组件里。
图4 插件通信与状态管理
```js /** * 引擎播放状态 */ class PlayStatus { constructor() { this.IDLE = 'idle'; this.PLAYING = 'playing'; this.PAUSED = 'paused'; this.ERROR = 'error'; this.SEEKING = 'seeking';
this._status = this.IDLE;
}
setStatus(status)
{
this._status = status;
}
getStatus()
{
return this._status;
}
isPlaying()
{
return this.getStatus() == this.PLAYING;
}
isPaused()
{
return this.getStatus() == this.PAUSED;
}
...
}
export default new PlayStatus(); ```
3.3 插件内部实现
插件根据其复杂度,可以是一个简单类,也可以独立成一个小型SDK。以播放引擎为例,不同的引擎基于引擎基类,包装了不同场景下的播放需求,以实现水平扩展;引擎SDK提供一个工厂方法创建引擎实例,EnginePlugin
负责引擎实例的创建和管理。
HttpEngine
负责HLS的播放FlashEngine
负责低版本浏览器下的播放PtEngine
负责自有格式直播流的播放FlvEngine
负责低延迟直播流的播放
图5 EngineSDK实现
3.4 视图插件的DOM容器管理
播放器DOM容器是外部创建的,播放器本身不应该改变容器宽高。所以每个插件通过垂直方向的层级划分来实现视图上的互不影响。
插件容器都是绝对定位的div
视图插件的层级预先定义
不同视图插件的子元素层级不能交错
图6 插件DOM分层
元素拦截鼠标冒泡事件
对于低版本浏览器,如果插件功能无法由JavaScript实现,会回退成Flash版本。由于<object>
元素会拦截鼠标事件,导致鼠标事件无法正常冒泡。如果上层<object>
没有可见元素,需要设置为不可点击。
3.5 播放器整合后的阶段性小结
整合后带来的优势
1. 开发效率的提升。从四个播放器到一个,开发过程极大地简化,开发人员可以更加专注于功能的实现。同时可复用的逻辑上升为组件,进一步提升开发效率。
2. 可扩展性的增强。出现新的业务需求,只需增加插件。对于复杂的功能,可以实现为一个小型SDK,在SDK内部增加额外功能实现。
带来的局限性和取舍
1. 低级浏览器下的某些体验的缺失。比如不支持在IE11下进行播放器全屏显示,或者在更低的IE版本下,H5的样式可能会相比纯Flash播放器的样式观感上略显生涩一些。
2. 由于功能的拆解,从客观上来说会加载多一些的文件。
针对第一点,通过对用户浏览器的UA分析,高级浏览器占绝大多数,并且随着浏览器版本的迭代, 上述问题的影响也会越来越小。而第二点,在后续的迭代优化中,我们会延迟加载和播放相关度不高的模块,确保不影响用户看到视频首帧的时间。
4. 总结
经过改造后,通用化的播放器架构已经形成;作为一款直播播放器,我们的定位是,专注于提供高性能,高可用性的播放能力,并且不断打磨播放器的易用性和用户体验,提供适应不同场景下的功能插件,以此满足爱奇艺不同业务不同场景下的直播需求。
随着浏览器对WebAssembly等新技术的支持,H.265和AV1等Codec的出现,播放器仍将继续改进,满足用户对于流畅播放、以及高画质的追求。
参考链接
[^1]: flv.js, https://github.com/Bilibili/flv.js
[^2]: Flash & The Future of Interactive Content,
end
你可能还想看
扫一扫下方二维码,更多精彩内容陪伴你!
爱奇艺技术产品团队
简单想,简单做